iT邦幫忙

2024 iThome 鐵人賽

DAY 24
1

https://ithelp.ithome.com.tw/upload/images/20241008/20168201LRZdNt4SnF.png

今天要介紹的是 PRPL 模式,這也是和優化效能有關的模式。
PRPL 模式是由 Google Chrome 團隊所發展提出的,目的是提供更快的網絡體驗。這個模式是隨著 Service Workers、後台同步、Cache API、Priority hints 和 pre-fetching 等技術出現而發展出來的,此模式可確保在低端設備或網路不佳的情況下,應用程式也能正常載入。PRPL 模式包括 4 個主要操作:Push(or preload)、Render、Pre-cache 和 Lazy-load,因為 PRPL 就是關注這 4 個要素:

  • Push (or Preload) 關鍵資源,最大化減少往返 server 的次數,降低載入時間
  • 盡快 Render 初始路由,以改善使用者體驗
  • Pre-cache 經常存取的資源(assets),最大化減少對 server 的請求數,達成更好的離線體驗
  • Lazy load 其他非關鍵性的路由和資源(assets)

P:Push (or Preload) 關鍵資源

Preload

我們可為關鍵資源加上 preload 的 resource hint,告訴瀏覽器要盡早獲取這些資源,在 HTML 文件的 head 中加入包含 rel="preload"<link> 標記,即可告訴瀏覽器預先載入此關鍵資源。

<link rel="preload" href="example-image.jpg" as="image">
<link rel="preload" href="emoji-picker.js" as="script">

而如果要在 React 中使用 preload,React 官方有提出 Canary 和實驗性版本的 preload API,不過因為是實驗性質,正式環境可能還不適合使用。React 官方有提及,如果你是用 React-based 的框架來開發(例如:Next.js、Remix),通常框架都會幫你處理好資源的載入,因此開發者很少需要自己呼叫 preload 的 API。如果要在 React 使用 preload 的方法,可以這樣用:

// 從 react-dom 匯入 preload
import { preload } from 'react-dom';

function AppRoot() {
  // preload 第一個參數是 href,第二個參數是 options,可填入的 options 請見官方文件 https://react.dev/reference/react-dom/preload#parameters
  preload("https://example.com/font.woff2", {as: "font"});
  // ...
}

hrefas attribute

<link>href 屬性會填入此資源的路徑,as 的屬性則代表此資源的類型,as 属性可能的值包含:audiofontimagescriptstyleworker 等,而 as 屬性填入的資源類型也會影響瀏覽器對於資源的載入順序。以下是 Chrome 對各資源的載入優先級排序。

表 1 Chrome 針對各資源的載入優先級排序(資料來源:https://web.dev/articles/fetch-priority)

資源類型 載入優先級
主資源 VeryHigh
CSS(early)** VeryHigh
Font VeryHigh
Script(early 或不是來自 preload scanner) *** High
Image(位於可視區域,在佈局完成後發生) High
XHR/fetch(async) High
CSS(late)** Medium
Image(前5個超過10,000px²的圖片) Medium
Script(async/defer) Low
Image Low
Media (video/audio) Low
CSS(media mismatch)**** Low
Prefetch VeryLow

** early & late:early 指的是在任何非預加載的圖片被請求之前就發出的請求。網頁載入時,瀏覽器會先處理一些資源(如CSS、JS等)。如果這些資源是在非預加載的圖片被請求之前就已經請求了,那這些資源就被視為早期資源,例如在 <head> 中的 CSS。相對而言,late 指的是在非預加載的圖片請求之後。
*** 不是來自 preload scanner:preload scanner 是瀏覽器中的一個機制,它能夠在 HTML 解析的過程中,並行預加載某些資源。不是來自 preload scanner 代表這些 script 不會通過 preload scanner 提前載入,而是需要等到 HTML 解析到它們的時候才會被請求。
**** media mismatch:指的是那些媒體類型不匹配的 CSS。媒體類型(media type)通常由 CSS 的 media 屬性定義,例如 media="screen" 或 media="print"。當 media 尺寸不符合時,瀏覽器會忽略這些文件的預加載,因為這些樣式暫時不適用於當前的設備或環境。

另外,關於圖片載入的優先級,在預設情況下,圖片載入的優先級是 Low,但如果它們在可視範圍內,佈局完成後會提升為 High。Chrome 117 開始,前 5 張大圖片的優先級會是 Medium,這樣可以加快它們的載入速度。如果你希望一些關鍵圖片一開始就以 High 優先級加載,可以使用 fetchpriority="high" 屬性來加速它們的載入。

Preload 與 Prefetch

preload 與 prefetch 都是用來預先載入資源的 resource hint,兩者差異如下:

表 2 preload 與 prefetch 差異(資料來源:自行繪製)

preload prefetch
急迫性 強制性、急迫性更高,無論如何都會被預載入 屬於 Resource Hint,建議瀏覽器去做,較不急迫;瀏覽器會根據網路連線和 bandwidth 是否足夠來決定是否要預取資源
資源類型 通常是需要立即使用的資源,如初始渲染中使用的字體,或使用者立即能看到的某些圖片 抓取的資源不限於當前頁面使用,可以跨越頁面
載入時機 現在立刻馬上就要下載 什麼時候要下載,交由瀏覽器自行決定

Preload + async 技巧

可用 preload 搭配 async 來讓瀏覽器高優先下載 script,又不阻擋剖析器等待 script。

<link rel="preload" href="example.js" as="script">
<script src="example.js" async></script>
  • <link rel="preload">:告訴瀏覽器盡早開始下載指定的資源,讓它能提前載入
    • 此標籤本身不會執行資源或將其插入到 DOM 中,它只是預載入
    • 目的:提前下載資源,提高載入優先級
  • <script async>:用來執行 script
    • 當瀏覽器遇到 <script src="example.js" async> 時,如果這個資源已經被 <link rel="preload"> 預載入過,瀏覽器會直接使用已經下載的資源,而不會再次下載
    • 目的:非同步下載 script,避免阻塞 HTML 的解析,但 script 下載完成後會立即執行,此時會暫時中斷 HTML 解析

兩者結合使用時,瀏覽器會先預載入資源,然後再執行,並且不會重複下載同一個資源。

使用 Preload 的建議與注意事項

《JavaScript 設計模式學習手冊 第二版》一書中有提及對 preload 的建議與注意事項如下:

  • 一般來說,preload 會按照撰寫的順序載入
    • 任何優先級大於或等於 Medium 的 preload 都應小心放置於 HTML <head>,以免影響其他更緊急資源的載入
  • 字體的 preload 最好放在 <head> 的結尾或 <body> 的開頭
    • 可確保頁面主要的結構性內容能先載入,而字體的載入能跟隨其後,避免因為字體載入過早而影響其他重要資源的載入
  • 圖片的 preload 會有較低的優先級,應該合理安排圖片 preload 的順序,確保它不會拖慢網頁中更關鍵資源的載入速度,例如影響網頁互動的 JavaScript

Preload 的優缺點

  • 優點
    • 可預先載入互動需要的 JavaScript bundle
    • 可優化互動時間(Time To Interactive, TTI)或首次輸入延遲(First Input Delay, FID)這類指標
  • 缺點
    • 過度使用 preload 改善互動性時,可能會延遲其他資源(如:圖片、字體)的載入,而影響了首次內容繪製(First Contentful Paint, FCP)或最大內容繪製(Largest Contentful Paint, LCP)的指標,因此須謹慎使用 preload

Push

在訪問網站時,一般流程如下:

  1. 瀏覽器向伺服器請求所需資源,得到初始 HTML 檔案
  2. 瀏覽器剖析 HTML,發現需要 style 或 script,再向伺服器發請求以獲取這些資源
  3. 拿到資源後,再以此渲染畫面

https://ithelp.ithome.com.tw/upload/images/20241008/20168201BjE3D3yRZC.jpg
圖 1 訪問網站的一般流程(資料來源:自行繪製)

這個訪問網站流程有哪裡可優化呢? 可以看出瀏覽器需要多次向伺服器請求資源(style、script),多次向伺服器請求資源會增加網路往返的次數(Round Trip Time, RTT),導致整個網頁的載入時間變長,因此我們應避免多次向伺服器請求資源,減少瀏覽器和伺服器間的往返次數。而其中一個優化方式就是利用 HTTP/2 協議的 Server Push 技術。

Server Push

原先是瀏覽器爬取 HTML 時發現還要 style 和 script 檔案時,再向伺服器發出請求,但 server push 可做到讓伺服器自動發送額外資源給客戶端,客戶端就不需要每次明確請求資源,節省了網路往返的次數。流程如下:

  1. 瀏覽器向伺服器請求 index.html
  2. 伺服器回應 index.html,並另外 push style.cssindex.js
  3. 瀏覽器將資源儲存在瀏覽器快取中
  4. 當瀏覽器剖析 index.html 檔案發現需要 style.cssindex.js,就可從快取中取得資源,不須再向伺服器請求

https://ithelp.ithome.com.tw/upload/images/20241008/201682013KqQRqsEh0.jpg
圖 2 Server Push 的流程(資料來源:自行繪製)

不過 Server Push 也有其缺點,server push 無法感知 HTTP cache,有時即使 HTTP header 中關於快取的資訊顯示此資源還沒過期,伺服器仍然會照樣推送,導致資源重複推送,client 端重複下載一樣的、沒過期的資源,造成網路頻寬的浪費。另外,server push 能推送的資源類型是有限制的,資源需要能被快取,且不能帶有 response body,因此只能推送 GET 與 HEAD 的請求。
PRPL 模式對此的解法是,在初始載入後使用 Service Worker 來快取資源,在瀏覽器發出請求時攔截不必要請求,在伺服器推送重複資源時避免下載、剖析不必要的資源。

補充:Service Worker
Service Worker 屬於 Web Workers 的一種,它是一層瀏覽器與網路請求間的 proxy,可攔截瀏覽器請求並快取資源,更多介紹此篇文章後半會再提及。

R:Render

如何盡快渲染(Render)初始路由?可參考以下方式。

1. 降低初始載入的 bundle 大小

可以將 bundle 拆分為更小的檔案,只載入需要資源的 bundle,這部分在前面的程式碼拆分文章有提過如何拆分 bundle,並依照需求載入。
而除了拆分程式碼,另外一個降低初始 bundle 大小的方式,就是讓某些 JavaScript 的程式碼在伺服器端執行,而不是傳到瀏覽器再執行,以此減少傳給瀏覽器的 bundle 大小,這和渲染模式比較有關,會在之後文章繼續說明。

2. 最大限度利用快取

快取時要注意的是,如果快取了一個較大的 bundle 可能會遇到一個問題,就是多個 bundle 間可能會共享相同的資源,瀏覽器會無法辨別 bundle 內哪些部份是被共享的,這些共享資源可能會因為每個 bundle 的不同而重複下載,無法被有效快取,因此增加了與伺服器的往返次數,影響效能。
PRPL 模式因此強調每次請求的 bundle 應包含當前所需的最少資源,確保這些資源可以被快取。
如果 bundle 過大或過於複雜,可能會降低效能。
其他關於快取的敘述請見下方。

P:Pre-cache

快取(cache) 是什麼

Proxy 模式的文章中有提到快取這個概念,快取的概念就是先在某個空間儲存需要被經常存取的資料,當之後需要請求這資料時,會先到這個快取的儲存空間尋找,如果有就直接拿該資料,如果沒有才發出請求去取得資料。
https://ithelp.ithome.com.tw/upload/images/20241008/20168201Hit03lNhii.jpg
圖 3 快取示意圖(資料來源:自行繪製)

如果大略分類網頁應用的快取類型,可以分為以下(以下按照的順序是當使用者從瀏覽器發出請求時,會經過的順序):

  1. Service Worker Cache
  2. Disk Cache (HTTP Cache/Browser Cache)
  3. CDN Cache

快取的詳細介紹可參考文末 Reference 的資源,這裡先不展開太多。

回到 Pre-Cache,其意思是我們可以預先快取一些使用者未來會經常需要存取的資源,之後使用者需要時就能盡快拿到這資料,又或是如果使用者突然斷網,也可以透過快取取得資料而不會畫面突然空白。而要如何做到呢?我們可以用 Service Worker 來做這類的快取控制,會是 Service Worker 而不是 HTTP cache 或 CDN cache,是因為開發者可以對它有很高的控制權,能撰寫程式碼來控制快取的條件、行為等等,也能因此達到離線瀏覽的功能。

Service Wroker 是什麼

Service Worker 屬於 Web Workers 的一種。Web Workers 是什麼?在瀏覽器環境中,除了執行 JavaScript 的 JacaScript 引擎,還有其他運作的引擎如:渲染 engine、處理 HTTP request 的引擎等,這些引擎可以不受 JavaScript 程式的影響而在背後執行自己的任務,而 Web Workers 可以想成是另一個執行任務的引擎,他會在瀏覽器開出一條新的 thread 來執行 JavaScript,兩條 thread 不會互相影響,因此不會干擾使用者介面的運行。

Service Worker 又是什麼呢?如上所述,它屬於 Web Workers 的一種,可以在瀏覽器背後執行 JavaScript,獨立於主瀏覽器線程(main thread)之外。其特色在於它會在 Web 應用程式與網絡之間扮演一層 proxy,透過監聽 fetch 事件,它能攔截瀏覽器發出的網絡請求,且它能利用 Cache API 來達到快取功能,可以在攔截瀏覽器發出的請求後決定要不要回傳快取的內容。
擁有正確緩存/快取策略的 Service Worker 可以為各種情境提供更好的使用者體驗,例如:立即回傳 pre-cache 的資源、在快取中儲存資料,以及在連接到網路時更新資料。
https://ithelp.ithome.com.tw/upload/images/20241008/20168201zU76Q4BbDP.jpg
圖 4 Service Worker 示意圖(資料來源:自行繪製)

怕篇幅展開太多,這篇先不介紹 Service Worker 的實作範例,對如何自己撰寫 Service Worker 程式碼有興趣的可參考 Day18 X Service Workers CacheService Worker:從入門到放棄,但如果不想要自己寫複雜的程式碼,也可以使用 Google 提供的服務 Workbox,它提供了一系列工具,可讓開發者更快速的建立和維護 Service Worker 來快取資料。

L:Lazy-load

在需要的時候才惰性載入路由、bundle 或是靜態資料,這部分在動態匯入的文章有介紹過,就不再多說。但這裡提一下在 <img> 標籤中我們可以加入 loading 的屬性來設定是要惰性插入還是要急迫載入此圖片。

<img src="example.png" loading="lazy" alt="…" width="150" height="150">

loading 屬性可填入兩種值:

  • lazy:延遲載入,直到資源到達距離可視區域(viewport)一定距離時才載入
  • eager:預設行為,沒填入 loading 屬性時預設是 eager,不論圖片位於頁面的哪個位置,都會立即載入。雖然是預設值,但在某些情況下可以明確設定,例如如果你的程式碼檢查工具對沒有明確設置 loading 屬性而報錯時,就可設定此值

更多關於圖片 loading 屬性的說明可見此篇文章

小結

總結來說,PRPL 對網頁應用的建議是:

  • 先確保初始路由在使用者設備上可見,在這之前不會請求其他資源
  • 載入初始路由後,可 install service worker 來在背景取得會經常存取的資源,service worker 在背後存取資源,不會讓使用者感到延遲
  • 當使用者想導航到其他路由,此路由資源有先被 service worker 快取時,就可從 service worker 的快取資源中取出,而不需向伺服器發送請求
  • 對不經常存取的路由,則採用動態載入

Reference


上一篇
[Day 23] 程式碼拆分(Code Splitting)與動態匯入(Dynamic Import) (2)
下一篇
[Day 25] 渲染模式初探與效能指標介紹
系列文
30天的 JavaScript 設計模式之旅30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言